dart lang

2022-12-05 · 12 min read

variables #

mutable #

var name = 'Bob';
Object name = 'Bob';
String name = 'Bob';
var name = const 'Bob';
name = 'Alice';

immutable #

final name = 'Bob';
final String name = 'Bob';
name = 'Alice'; // << compile error: final var can only be set once
final String name = const 'Bob';

comptime #

const bar = 1000000;
const double atm = 1.01325 * bar;
const p1 = [bar, atm];
const p2 = const [bar, atm]; // << style: const-value is redundant

If you enable null safety, then you must initialize the values of non-nullable variables before you use them.

The compiler can sometimes determine a lazy-init val is not null before use:

int foo;
if (isFooable) {
	foo = 123;
} else {
	foo = 0;
}

print(foo);

late init #

Use the late modifier if dart can't prove non-null-before-use. You will get a runtime panic if it's null on use:

late String desc;

void main() {
	desc = 'hello';
	print(desc);
}

lazy init #

You can also use late for lazy-init:

// only computed on first use
late String expensiveValue = computeExpensive();

built-in types #

numbers (num, int, double) #

  • class num
  • (native) int := i64
  • (native) double := double
  • both int extends num and double extends num

strings (String) #

String is a sequence of UTF-16 code points. You can use either '' or "" to create a string:

var s1 = 'hello';
var s2 = "world";

Dart also supports string interpolation:

var inner = "world";
var s1 = "hello $inner";
var s2 = "HELLO ${inner.toUpperCase()}";

lists (List<T>) #

List<int> list = [1, 2, 3];
assert(list.length == 3);
assert(list[1] == 2);

var list = const [1, 2, 3];
list[1] = 1; // << error

spread operator #

  • spread operator ...
  • null-aware spread operator ...?
var xs = [1, 2, 3];
var ys = [0, ...xs];
assert(ys == [0, 1, 2, 3]);

var zs = null;
var ws = [0, ...?zs];
assert(ws == [0]);

collection control flow operators #

var nav = ["Home", "Furniture", "Plants", if (promo) "Outlet"];
var ints = [1, 2, 3];
var strs = ["#0", for (var i in ints) "#$i"];
assert(strs == ["#0", "#1", "#2", "#3"]);

sets (Set<T>) #

var halogens = {"F", "Cl", "Br", "I", "At"};
var names = <String>{};
Set<String> names = {};
names.add("Alice");
names.addAll({"Bob", "Charlie"});

maps (Map<K, V>) #

var halogens = {
	9: "F",
	17: "Cl",
};

assert(halogens.length == 2);
assert(halogens[9] == "F");
assert(halogens[2] == null);

symbols (Symbol) #

  • Represents an operator or identifier in a dart program.
#radix
#bar

example using Function.apply:

abstract class Function {
	external static apply(
		Function function,
		List<dynamic>? positionalArguments,
		[Map<Symbol, dynamic>? namedArguments],
	);
}

void printWineDetails(int vintage, {String? country, String? name}) {
  print('Name: $name, Country: $country, Vintage: $vintage');
}

Function.apply(printWineDetails, [2018], {#country: "USA", #name: "Casocco"});

functions (Function) #

Dart functions are first-class objects and can be assigned to variables and passed as arguments.

bool isNoble(int n) {
	return _nobleGases[n] != null;
}

// can omit type annotations...
isNoble(n) {
	return _nobleGases[n] != null;
}

// single-expressions shorthand syntax
bool isNoble(int n) => _nobleGases[n] != null;

parameters #

  • Functions can have required positional parameters, followed by named parameters xor optional positional parameters.
named parameters #
  • Named params are null by default (hence the bool? for each param in this example):
void enableFlags({bool? bold, bool? hidden}) { /* .. */ }

enableFlags(bold: true, hidden: false);
  • You can specify default values:
void enableFlags({bool bold = false, bool hidden = false}) { /* .. */ }

enableFlags(hidden: true); // bold = false
  • You can use the required keyword to make a named param non-optional:
const Scrollbar({super.key, required Widget child});
optional positional parameters #
void say(String from, String msg, [String? device]) {
  final tail = (device != null) ? "on their $device" : "";
  print("$from says $msg$tail");
}

void main() {
  say("Bob", "hello");
  say("Alice", "goodbye", "Android");
}

main #

void main() {}
void main(List<String> arguments) {}
  • You can use the args package for arg parsing.

closures #

syntax:

([[Type] param1[, ...]]) { codeBlock; }
([[Type] param1[, ...]]) => expression;

const printer = (val) { print("this is $val"); };

["bob", "Alice"]
	.map((name) => name.toUpperCase())
	.forEach((name) => print("big name: $name"));

Function makeAdder(int addend) => (int i) => i + addend;

operators #

(only highlighting interesting or non-standard operators)

  • ~/ integer division, e.g., 5 ~/ 2 == 2.
  • x is T is true if value x impls T. x is Object? is always true.
  • x is! T == !(x is T)
  • x ??= value assigns value iff x is null.

ints are always signed, so shifts are sign-extended as well. To do a bit-wise unsigned right shift, use >>>.

  • expr1 ?? expr2 returns expr1 if it's non-null, otherwise expr2.

cascade notation lets you make a sequence of operations on one object:

var paint = Paint()
  ..color = Colors.black
  ..strokeCap = StrokeCap.round
  ..strokeWidth = 5.0;

// if the object is possibly null, then use `?..` for the first operation
querySelector("#confirm")
  ?..text = "Confirm"
  ..classes.add("important")
  ..onClick.listen((e) => window.alert("Confirmed"))
  ..scrollIntoView();

other maybe-null operators

xs?[1] == xs.map(|xs| xs[1])
paint?.color == paint.map(|p| p.color)
paint! == paint.unwrap()
paint!.color == paint.unwrap().color

you can override operators on classes

class Vec3d {
  final double x, y, z;

  const Vec3d(this.x, this.y, this.z);

  Vec3d operator +(Vec3d v) => Vec3d(x + v.x, y + v.y, z + v.z);
}

control flow #

for (var i = 0; i < 10; i++) { /* .. */ }
for (final cb in callbacks) { /* .. */ }
callbacks.forEach((cb) => /* .. */);
while (foo) { /* .. */}
  • supports c-style switch-case blocks...

All of Dart’s exceptions are unchecked exceptions. Methods don’t declare which exceptions they might throw, and you aren’t required to catch any exceptions.

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  // A specific exception
  buyMoreLlamas();
} on Exception catch (err) {
  // Anything else that is an exception
  print('Unknown exception: $err');
} catch (err, backtrace) {
  // No specified type, handles all
  print('unknown error:\n$err\ntrace:\n$backtrace');
  rethrow;
} finally {
  cleanup();
}

classes #

The root of the Dart object hierarchy is Object? := { null U Object }:

				Object?
				|     |
			  null  Object
			        / / \ \
			    <normal classes>

Classes have one superclass (by extends) and zero or more mixin interfaces.

constructors #

import "dart:math";

class Point {
  final double x;
  final double y;
  final double z;

  // initializing with "formal" parameters
  const Point({required this.x, required this.y, this.z = 0.0});

  // initializing with C++-style initializer list
  Point.fromPolar({required double r, required double theta})
      : x = r * cos(theta),
        y = r * sin(theta),
        z = 0.0;

  // can also redirect to another constructor
  Point.fromPolar2({required double r, required double theta})
      : this(x: r * cos(theta), y: r * sin(theta));


  // if you need to do some work before construction, use `factory`.
  // this also works for objects retrieved from e.g. an internal cache.
  factory Point.fromPolar3d({
    required double r,
    required double theta,
    required double phi,
  }) {
    final rSinPhi = r * sin(phi);
    final x = rSinPhi * cos(theta);
    final y = rSinPhi * sin(theta);
    final z = r * cos(phi);
    return Point(x: x, y: y, z: z);
  }

  @override
  String toString() => "{ $x, $y, $z }";
}

print(Point(x: 1, y: 2)); // { 1.0, 2.0, 0.0 }
print(Point(x: 1, y: 2, z: 3)); // { 1.0, 2.0, 3.0 }
print(Point.fromPolar(r: 3.0, theta: 0.25 * pi)); // { 2.121, 2.121, 0.0 }
print(Point.fromPolar3d( // { 0.812, 0.812, 2.772 }
  r: 3.0,
  theta: 0.25 * pi,
  phi: 0.125 * pi,
));

getters and setters #

class Point {
  const Point(this.x, this.y, this.x);
  final double x, y, z;

  double get length => sqrt(x * x + y * y + z * z);
  set length(double newLen) {
    final mult = newLen / length;
    x *= mult;
    y *= mult;
    z *= mult;
  }
}

bool approxEq(double x, double y) => abs(x - y) <= 1e-10;

final point = Point(0, sqrt(2)/2, sqrt(2)/2);
assert(approxEq(1.0, point.length));

implicit interfaces #

All classes define an implicit interface containing all its instance members and implemented interfaces.

// A person. The implicit interface contains greet().
class Person {
  // In the interface, but visible only in this library.
  final String _name;

  // Not in the interface, since this is a constructor.
  Person(this._name);

  // In the interface.
  String greet(String who) => 'Hello, $who. I am $_name.';
}

// An implementation of the Person interface.
class Impostor implements Person {
  String get _name => '';

  String greet(String who) => 'Hi $who. Do you know who I am?';
}

extension methods #

extension methods == point-free function -> dot method

Extension methods are pure syntactic sugar. They let us define faux-methods on a type (using dot notation) when we would otherwise be restricted to a static point-free function. In the following example, we can use extension methods to make parseInt("123") into a faux-method on String, like "123".parseInt().

// point free version
int parseInt(String s) => int.parse(s);

// a named extension on String
extension NumberParsing on String {
  int parseInt() => int.parse(this);
}

// an unnamed, module-local extension
extension on String {
  int parseInt() => int.parse(this);
}

assert(int.parse("123") == 123);
assert(parseInt("123") == 123);
assert("123".parseInt() == 123);

You can't invoke extension methods on values with type dynamic.

If two modules provide two extension methods of the same name on the same type, you will need to either (1) hide the extension for one of the modules or (2) namespace one of the modules with as.

import "string_apis.dart"; // defs String.parseInt() extension
import "string_apis_2.dart" hide NumberParsing2; // need to hide the second ext

"42".parseInt();
import "string_apis.dart"; // defs NumberParsing ext
import "string_apis_3.dart" as foo; // defs NumberParsing ext

"42".parseInt(); // << ERROR
NumberParsing("42").parseInt();
foo.NumberParsing("42").parseInt();

enums #

Dart has basic C-style enums, like:

enum Color { red, green, blue }

There's also "enhanced enums" which are NOT like real discriminated unions. This feature just allows enums to have methods and a fixed set of fields shared across all variants.

Update(2023/07/14): dart 3 now includes sealed and final classes, plus exhaustive pattern matching. These two features let us mostly emulate real discriminated unions. For example, consider an enum like Rust's Result<T, E>:

pub enum Result<T, E> {
  Ok(T),
  Err(E),
}

We can now mostly emulate this in dart 3:

sealed class Result<T, E> {
  const Result();

  T? get ok;
  E? get err;
}

final class Ok<T, E>(this.ok) extends Result<T, E> {
  const Ok(this.ok);

  @override
  final T ok;
  @override
  E? get err => null;
}

final class Err<T, E>(this.ok) extends Result<T, E> {
  const Err(this.err);

  @override
  final E err;
  @override
  T? get ok => null;
}

Using it isn't too bad with exhaustive pattern matching:

final Result<int, String> result = Ok(123);

// using switch-expressions:

final bool isOk123 = switch (result) {
  Ok(:final ok) => ok == 123,
  Err() => false,
};

assert(isOk123);

// or switch-statements:

final int value;
switch (result) {
  case Ok(:final ok):
    value = ok;
  case Err(:final err):
    print("Error: $err");
    return;
}

print("value = $value");
// value = 123

mixins #

mixins are like abstract classes that can neither be extended nor constructed.

(philip): looking through dart-lang std, it appears that mixins are almost never used? even interface-only cases, like Comparable<T>, are just abstract classes. consumers provide the interface by e.g. class X implements Comparable<X>.

(philip): more std: main use case in std seems to be type-specialization? like "implement List<int> more efficiently for Uint8List".

enum Ordering { less, equal, greater }

mixin PartialOrd<T> {
  Ordering? partialCmp(T other);
}

mixin Ord<T> on PartialOrd<T> {
  Ordering cmp(T other);
}

class BigInt with PartialOrd<BigInt>, Ord<BigInt> {
  final List<int> limbs;

  const BigInt() : this.limbs = const [];

  @override
  Ordering? partialCmp(BigInt other) => cmp(other);

  @override
  Ordering cmp(BigInt other) {
    final lenThis = this.limbs.length;
    final lenOther = other.limbs.length;

    if (lenThis < lenOther) {
      return Ordering.less;
    } else if (lenThis > lenOther) {
      return Ordering.greater;
    } else {
	  for (var i = lenThis - 1; i >= 0; i--) {
        final limbThis = this.limbs[i];
        final limbOther = other.limbs[i];

        if (limbThis < limbOther) {
          return Ordering.less;
        } else if (limbThis > limbOther) {
          return Ordering.greater;
        }
      }

      return Ordering.equal;
    }
  }
}

libraries #

// dart-core packages are prefixed with "dart:"
// like: `use std::collect::*;`
import "dart:collection";
// external packages are prefixed with "package:"
// like: `use serde::de::*;`
import "package:lib1/lib1.dart";

// namespace import
// like: `use std::collection as std_collection;`
import "dart:collection" as stdCollection;

// only import parts of library
// like: `use std::collection::{VecDeque, BTreeMap};`
import "dart:collection" show Queue, Set;

// import all but hide some things
import "dart::collection" hide Queue, Set;

async/await #

Future<String> lookup() async => "123";
Future<String> lookup() async => await http.fetch("google.com");

Dart uses a single-threaded async runtime, though you can spawn background workers with no shared state that communicate solely through message passing.

enable the unawaited_futures lint #

In stock dart, you can call async methods without awaiting them, which is super suprising to me. Enabling the unawaited_futures lint will tell the compiler to yell at you if you accidentally call an async method without awaiting it. This is like the #[must_use] attribute in Rust but only for dart's Futures.

Be sure to enable this lint early on in a project, since a lot of standard flutter code likes to play fast and loose with ignoring futures.

Note that there are some occasions where you want to call an async function but not await the returned future, like Navigator.push(), which actually returns a Future<T?> for the value the new route eventually yields when it Navigator.pop()s. In this particular case, the method synchronously pushes the new route onto the navigation stack and just returns a future for the popped value. If you don't care about the value, use the dart:async unawaited() function, which ignores the returned future:

unawaited(Navigator.push(context, route));

You should only use unawaited on a future if you're sure it won't return an error. If you don't care about the result regardless, you can use FutureExtensions.ignore like:

import 'dart:async' show FutureExtensions;

Navigator.push(context, route).ignore();

Unfortunately, this lint only tells you about unawaited futures... in async code. Which is why you should also...

enable the discarded_futures lint #

This is like unawaited_futures but also works in sync code. It should be renamed to unawaited_futures_sync IMO. Or unawaited_futures should just work in both contexts...